package hikari.managers;

import com.gooddies.wiring.annotations.Wire;
import com.gooddies.wiring.annotations.WiringComponent;
import hikari.gui.panels.HierarchyTree;
import hikari.gui.panels.ModifyPanel;
import hikari.objects3d.Basic3dObject;
import hikari.objects3d.NavMesh3dObject;
import hikari.pathfind.PathFindAgent;
import hikari.pathfind.PathFindUtils;
import hikari.pathfind.PolygonProcessor;
import hikari.pathfind.Recalculator;
import hikari.pathfind.data.CellData;
import hikari.pathfind.data.editor.FPoint;
import hikari.pathfind.data.editor.FRectangle;
import hikari.pathfind.data.editor.IndexPair;
import hikari.pathfind.data.editor.PathFindParams;
import hikari.pathfind.data.editor.PolygonConnection;
import hikari.pathfind.data.editor.Triangle;
import hikari.persistence.file.FileSection;
import hikari.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.swig.jirr.CMatrix4f;
import org.swig.jirr.ICustomizableTriangleMeshNode;
import org.swig.jirr.ISceneManager;
import org.swig.jirr.S3DVertex;
import org.swig.jirr.vector3df;

/**
 * @author Dmitry
 */
@WiringComponent(singleton = true, lazy = true)
public class NavmeshManager {

    private final List<CellData> cells = new ArrayList<>();
    @Wire
    private SelectionManager selectionManager;
    @Wire
    private Manager3D manager3D;
    @Wire
    private HierarchyTree hierarchyTree;
    @Wire
    private ModifyPanel modifyPanel;
    @Wire
    private Utils utils;

    private NavmeshManager() {
    }

    public void addNavmesh() {
        ISceneManager sm = manager3D.getSceneManager();
        ICustomizableTriangleMeshNode node = sm.addICustomizableTriangleMeshNode(sm.getRootSceneNode());
        node.getMaterial(0).setBackfaceCulling(false);
        NavMesh3dObject object = new NavMesh3dObject(node);
        manager3D.AddObject(object);
        utils.setModified();
        selectionManager.select(object);
    }

    private List<NavMesh3dObject> getNavmeshObjects() {
        final List<NavMesh3dObject> resultList = new ArrayList<>();
        hierarchyTree.forEachNodes(new HierarchyTree.HierarchyTreeNodeEnumarator() {
            @Override
            public boolean onNode(Basic3dObject node) {
                if (node instanceof NavMesh3dObject) {
                    resultList.add((NavMesh3dObject) node);
                }

                return true;
            }
        });

        return resultList;
    }

    public void clearNavmeshInfo() {
        cells.clear();
    }

    public void recalculatePath() {
        List<NavMesh3dObject> navmeshes = getNavmeshObjects();
        int index = 0;
        for (NavMesh3dObject node : navmeshes) {
            CellData data = processNavmesh(node);
            cells.add(data);
            index++;
        }
        recalculateCollisions();
    }

    private void recalculateCollisions() {
        Recalculator calculator = new Recalculator(getCells());
        calculator.recalculate();
    }

    public List<CellData> getCells() {
        return cells;
    }

    protected double findClosestLeftSide(double value) {
        int count = (int) (value / PathFindParams.CELL_SIZE);
        if (value < 0) {
            count--;
        }
        return PathFindParams.CELL_SIZE * count;
    }

    protected double findClosestRightSide(double value) {
        int count = (int) (value / PathFindParams.CELL_SIZE);
        if (value < 0) {
            count--;
        }
        return PathFindParams.CELL_SIZE * (count + 1);
    }

    private List<FPoint> vertexTo2dPoints(S3DVertex[] vertex, CMatrix4f transformation) {
        List<FPoint> points = new ArrayList<>(vertex.length);

        for (S3DVertex v : vertex) {
            vector3df position = v.getPos();
            vector3df newPosition = new vector3df(position.getX(), position.getY(), position.getZ());
            transformation.transformVector(newPosition);
            points.add(new FPoint(newPosition.getX(), newPosition.getZ()));
        }

        return points;
    }

    private CellData processNavmesh(NavMesh3dObject node) {
        CellData data = new CellData();
        ICustomizableTriangleMeshNode meshNode = node.getMeshSceneNode();
        int[] indicies = meshNode.getIndices();
        meshNode.updateAbsolutePosition();
        List<FPoint> vertex = vertexTo2dPoints(meshNode.getVertices(), meshNode.getAbsoluteTransformation());
        List<Triangle> triangles = createTriangles(indicies);
        List<IndexPair> outline = new PolygonProcessor().getPolygonOutline(triangles);
        FRectangle boundary = getPolygonBoundaries(vertex);
        data.setX(findClosestLeftSide(boundary.getMinX()));
        data.setY(findClosestLeftSide(boundary.getMinY()));
        data.setWidth(findClosestRightSide(boundary.getMaxX()) - data.getX());
        data.setHeight(findClosestRightSide(boundary.getMaxY()) - data.getY());
        data.setxCount((int) Math.round(data.getWidth() / PathFindParams.CELL_SIZE));
        data.setyCount((int) Math.round(data.getHeight() / PathFindParams.CELL_SIZE));
        data.createCells();
        updateCells(data, outline, vertex);
        //data.setX(data.getX() + meshNode.getAbsolutePosition().getX());
        //data.setY(data.getY() + meshNode.getAbsolutePosition().getZ());
        return data;
    }

    private void updateCells(CellData data, List<IndexPair> outline, List<FPoint> vertex) {
        double tx;
        double ty = data.getY();
        double cellSize = PathFindParams.CELL_SIZE;
        for (int j = 0; j < data.getyCount(); j++, ty += cellSize) {
            tx = data.getX();
            for (int i = 0; i < data.getxCount(); i++, tx += cellSize) {
                boolean isInPolygon = isCellInPolygonArea(tx, ty, cellSize, cellSize, outline, vertex);
                data.getCell(i, j).setActive(isInPolygon);
            }
        }
    }

    private boolean isCellInPolygonArea(double x, double y, double w, double h, List<IndexPair> outline, List<FPoint> vertex) {
        //first we check if cell collides with border of the polygon
        if (!isCellCollidesWithBorder(x, y, w, h, outline, vertex)) {
            return false;
        }
        //then we check, how much times ray from cell center collides with
        //boundaries to determine is cell inside or outside
        Set<Double> cpoints = getCollissionPointsOfHorizontalRayFromPointToLeftAndBoundaries(x + w / 2.0, y + h / 2.0, outline, vertex);
        return cpoints != null && (cpoints.size() % 2) != 0;
    }

    private Set<Double> getCollissionPointsOfHorizontalRayFromPointToLeftAndBoundaries(double x, double y, List<IndexPair> outline, List<FPoint> vertex) {
        Set<Double> cp = new HashSet<>();
        for (IndexPair pair : outline) {
            FPoint pos1 = vertex.get(pair.getIndex1());
            double x1 = pos1.getX();
            double y1 = pos1.getY();
            FPoint pos2 = vertex.get(pair.getIndex2());
            double x2 = pos2.getX();
            double y2 = pos2.getY();
            if (x1 > x && x2 > x) {
                continue;
            }

            FPoint point = PathFindUtils.getLineLineIntersection(x1, y1, x2, y2, x, y, -999999999999999.0, y);
            if (point != null) {
                cp.add(point.x);
            }
        }

        return cp;
    }

    private boolean isCellCollidesWithBorder(double x, double y, double w, double h, List<IndexPair> outline, List<FPoint> vertex) {
        for (IndexPair pair : outline) {
            FPoint pos1 = vertex.get(pair.getIndex1());
            double x1 = pos1.getX();
            double y1 = pos1.getY();
            FPoint pos2 = vertex.get(pair.getIndex2());
            double x2 = pos2.getX();
            double y2 = pos2.getY();

            if (PathFindUtils.linesIntersect(x1, y1, x2, y2, x, y, x + w, y)) {
                return false;
            }
            if (PathFindUtils.linesIntersect(x1, y1, x2, y2, x, y, x, y + h)) {
                return false;
            }
            if (PathFindUtils.linesIntersect(x1, y1, x2, y2, x, y + h, x + w, y + h)) {
                return false;
            }
            if (PathFindUtils.linesIntersect(x1, y1, x2, y2, x + w, y, x + w, y + h)) {
                return false;
            }
        }
        return true;
    }

    private FRectangle getPolygonBoundaries(List<FPoint> points) {
        double minX = 0;
        double maxX = 0;
        double minY = 0;
        double maxY = 0;

        if (points.size() > 0) {
            minX = points.get(0).x;
            minY = points.get(0).y;
            maxX = minX;
            maxY = minY;
        }
        for (FPoint p : points) {
            double x = p.getX();
            double y = p.getY();
            if (x < minX) {
                minX = x;
            }
            if (y < minY) {
                minY = y;
            }

            if (x > maxX) {
                maxX = x;
            }
            if (y > maxY) {
                maxY = y;
            }
        }
        FRectangle rect = new FRectangle();
        rect.setRect(minX, minY, maxX - minX, maxY - minY);
        return rect;
    }

    private List<Triangle> createTriangles(int[] indicies) {
        List<Triangle> triangles = new ArrayList<>();
        int indiciesCount = indicies.length;
        for (int i = 0; i < indiciesCount; i += 3) {
            triangles.add(new Triangle(indicies[i + 0], indicies[i + 1], indicies[i + 2]));
        }

        return triangles;
    }

    public void load(FileSection section) throws IOException {
        clearNavmeshInfo();
        if (section.getChunk("cellSize") != null) {
            float cellSize = section.readFloat("cellSize");
            PathFindParams.CELL_SIZE = cellSize;
        }
        int cellsCount = section.readInt("cellsCount");
        Map<Integer, CellData> intCellMap = new HashMap<>(cellsCount);
        //first we read all sections
        for (int j = 0; j < cellsCount; j++) {
            CellData cell = new CellData();
            FileSection cellSection = section.readSection("cell" + j);
            int cellId = cellSection.readInt("cellId");
            intCellMap.put(cellId, cell);
            cell.setxCount(cellSection.readInt("cellWidth"));
            cell.setyCount(cellSection.readInt("cellHeight"));
            cell.setX(cellSection.readFloat("x"));
            cell.setY(cellSection.readFloat("y"));
            cell.setWidth(cellSection.readFloat("width"));
            cell.setHeight(cellSection.readFloat("height"));
            cell.createCells();
            boolean[] values = cellSection.readBooleans("cellValues");
            int index = 0;
            for (int k = 0; k < cell.getyCount(); k++) {
                for (int i = 0; i < cell.getxCount(); i++) {
                    cell.getCell(i, k).setActive(values[index]);
                    index++;
                }
            }

            cells.add(cell);
            cellSection.close();
        }

        //second we read connections between sections
        for (int j = 0; j < cellsCount; j++) {
            FileSection cellSection = section.readSection("cell" + j);
            int cellId = cellSection.readInt("cellId");
            int connectedCellCount = cellSection.readInt("connectedCellsCount");
            CellData currentCell = intCellMap.get(cellId);
            for (int i = 0; i < connectedCellCount; i++) {
                PolygonConnection connection = new PolygonConnection();
                FileSection connectedCellSection = cellSection.readSection("connCell" + i);
                int connectedCellId = connectedCellSection.readInt("connectedCellId");
                CellData connectedCell = intCellMap.get(connectedCellId);
                connection.readConnectionPoints(connectedCellSection);
                currentCell.addConnectedPolygon(connectedCell, connection);
                connectedCellSection.close();
            }

            cellSection.close();
        }

        recalculateCollisions();
    }

    public void save(FileSection section) throws IOException {
        section.writeFloat("cellSize", PathFindParams.CELL_SIZE);
        section.writeInt("cellsCount", getCells().size());
        int index = 0;
        Map<CellData, Integer> cellsIndexMap = getCellDataIndexMap(getCells());
        for (CellData cell : getCells()) {
            FileSection cellSection = section.createNewSection("cell" + index);
            cellSection.writeInt("cellId", cellsIndexMap.get(cell));
            cellSection.writeInt("cellWidth", cell.getxCount());
            cellSection.writeInt("cellHeight", cell.getyCount());
            cellSection.writeFloat("x", (float) cell.getX());
            cellSection.writeFloat("y", (float) cell.getY());
            cellSection.writeFloat("width", (float) cell.getWidth());
            cellSection.writeFloat("height", (float) cell.getHeight());
            boolean[] cellsActivity = new boolean[cell.getxCount() * cell.getyCount()];
            int cellIndex = 0;
            for (int j = 0; j < cell.getyCount(); j++) {
                for (int i = 0; i < cell.getxCount(); i++) {
                    cellsActivity[cellIndex] = cell.getCell(i, j).isActive();
                    cellIndex++;
                }
            }

            cellSection.writeBooleans("cellValues", cellsActivity);
            int connectedCellCount = cell.getConnection().size();
            cellSection.writeInt("connectedCellsCount", connectedCellCount);
            int connectedIndex = 0;
            for (CellData connectedCell : cell.getConnection().keySet()) {
                PolygonConnection polygonConnection = cell.getConnection().get(connectedCell);
                FileSection connectedCellSection = cellSection.createNewSection("connCell" + connectedIndex);
                connectedCellSection.writeInt("connectedCellId", cellsIndexMap.get(connectedCell));
                polygonConnection.saveConnectionPoints(connectedCellSection);
                connectedCellSection.close();
                connectedIndex++;
            }

            cellSection.close();
            index++;
        }
    }

    private Map<CellData, Integer> getCellDataIndexMap(List<CellData> cells) {
        Map<CellData, Integer> map = new HashMap<>();
        for (int i = 0; i < cells.size(); i++) {
            map.put(cells.get(i), i);
        }

        return map;
    }

    public PathFindAgent createNavmeshAgent(Basic3dObject object) {
        PathFindAgent agent = new PathFindAgent();

        return agent;
    }

    public void addNavmeshAgentComponentToSelectedObject() {
        Basic3dObject object = selectionManager.getCurrentSelectedObject();
        if (object == null) {
            return;
        }

        addNavmeshAgentToObject(object);
    }

    public void addNavmeshAgentToObject(Basic3dObject object) {
        object.addHComponent(createNavmeshAgent(object));
        modifyPanel.reFillPanel();
    }
}
